Inheritance

From what you've learned so far, OOP isn’t a whole lot different than procedural programming. However, inheritance and polymorphism will change that, as they are two defining features of object-oriented programming. These concepts are so defining, in fact, that if someone says they use OOP, it usually means they use inheritance and polymorphism.

What is inheritance? It’s a way to organize classes into a hierarchy where data and logic can be shared and overridden. Specifically, you creates what is called a base class, containing the most general logic and data and derived classes that extend and modify the functionality of the base class. Some examples of this are (see the example program) having an “animal” base class that is extended by specific classes like “dog” and “cat.” Derived classes are said to “inherit” from their base class, and the base class is called the “parent” or just the base class of its derived classes.

In C++, any class can be inherited from—derived classes can themselves be used as base classes, multiple derived classes can inherit from the same base class, and a derived class can inherit from multiple base classes.

It’s common practice to diagram inheritance trees (as they’re called) like we diagrammed linked trees, although here, the links represent inheritance rather than pointers. For example, one could structure entities in a game as such:

Inheritance can be useful in many situations—entire languages, such as Java and C#, are centered around OOP—but I’m not trying to say OOP is the best programming paradigm to use in any situation. Ideally, you should assess what you’re trying to create and choose whichever method will be the most effective. Some people really like OOP, some abhor it. Personally, I’d be wary of overly complicated inheritance and polymorphism trees, as they can be difficult to work with and debug, and can impact performance.

The example program will be very helpful this week! Download it and follow how each step propagates through the inheritance tree.

Access Modifiers

We’ve already learned about how “public” and “private” specify data availability within classes. To rehash, “public” allows anything and anyone to access the method or data, and “private” allows only member functions of that class to access the method or data.

There is a third modifier we haven’t talked about: “protected.” Protected signifies that derived classes, as well as the class itself, can access the member. For example, if your “animal” base class contains a protected name member, all derived classes (e.g. “dog”) can access the name member. Private data, on the other hand, will not be accessible in a derived class—only the original classes' member functions may access it.

When creating classes to be used with inheritance, your data should primarily be protected for use in derived classes. However, private data may still be used for security. If you do not want your class to be inherited from, do not use protected.

Using Inheritance

Declaring an inherited class is quite simple in C++: begin like any other class, then add a colon, an access modifier, and your base class.

class dog : public animal { /* ... */ };
class monster : protected entity { /* ... */ };

Here, the access modifier has a different meaning; it represents how the base members are interpreted within the derived class. Inheriting as “public” will change nothing about the base class’s members—public members will remain public and protected will remain protected. Inheriting as “protected” will inherit public members as protected within the derived class. Protected members will still remain protected. Finally, inheriting as “private” will make both public and protected members of the base class private within the derived class.

After declaring your class as inherited, the rest of the syntax is exactly the same as we’ve learned. Implementing members is also the same, except that you can reference inherited data members and member functions. In the example, a member function of dog could call setName()—the function is inherited from animal.

Finally, instantiating your derived class is the same as always. You use the instance in the same way, except you can access public members from both the base class and the derived class. In the example, setName() is called from an instance of dog. This is OK, because dog inherits the function from animal.

class animal
{
public:
	animal();
	~animal();
	void setName(const char* n);

private:
	char name[10];
}

class dog : public animal 
{
public:
	dog();
	~dog();
	int size;
};

int main() {
	dog derived;

	derived.size = 10;
	derived.setName(“fido”);
}

Polymorphism

Inheritance allows you to extend functionality and create sub-types, but you are stuck with the base functionality of the parent class. Polymorphism is much more powerful. It allows you to abstract derived classes as their base class, generalizing interfaces while customizing logic. Essentially, when using polymorphism your bass class specifies an interface that all subclasses must support.

For example, if you created some polymorphic animals (see example), your base “animal” class specifies what functions will be available in all subclasses. Then, you can have your “dog” override a “speak” method to print “bark!,” whereas your “cat” can override the “speak” method to say “meow!” Furthermore, you can store all your “cats” and “dogs” within the same, single-typed data structure, under their shared base class “animal.”

In C++, polymorphism only works using pointers/dynamic memory. For example, if create an array that holds “animals,” all your “cats” and “dogs” will be converted to “animals” when added to the array. Any polymorphic features are lost. But, if you create an array that holds pointers to “animals,” your “cats” and “dogs” remain “cats” and “dogs” when you add their addresses to the array.

See the example program.

Virtual Functions

In C++, you must specifically define what member functions of a class are polymorphic and hence overrideable. A virtual function will be available in all derived classes, but it can be changed—overridden to do different things depending on the derived class.

If a derived class re-implements (overrides) a virtual function, the more specific version (“bark!”) will always be called. If the derived class doesn’t implement the function, the base class’s more general version will be called. For example, if you have a pointer to an "animal," which is technically a "dog," calling a virtual function will call the function from "dog" if it exists, and the function from "animal" if it doesn't.

The keyword “virtual” denotes functions as polymorphic. Any class with one or more virtual functions is considered polymorphic.

You are almost always going to want at your base class destructor to be virtual. This is a special case—when an object is destroyed, this tells the program to first call the destructor of any derived class, then the destructor of the base class. If the base destructor isn't virtual, the derived destructor will not be called, and the program may leak memory. Of course, remember that for other virtual functions, only the most specific version is called.

All of this is rather theoretical; I highly encourage you to check out the example program. Follow how each step invokes constructors, functions, and destructors.

class animal
{
public:
	animal();
	virtual ~animal();
	virtual speak();
} 

class dog : public animal
{
public:
	dog();
	~dog();
	speak();
}
 
int main() {
	dog* doge = new dog();
	animal* generalAnimal = doge;

	generalAnimal->speak();	        // Will call speak() from dog
	delete generalAnimal;	        // Will call ~dog() then ~animal()
}

Using Polymorphism

Using polymorphic, inherited classes is no different than using normal classes. On their own, members are accessed in the exact same way. However, with polymorphic classes, you can create data structures of general (base) data types and actually store the specific (derived) data types within—and have functionality be preserved. Remember, you must use pointers for polymorphic references to work.

Yet agian—see the example program. We create an array (vector) of animals and compare it to an array of animal*s in terms of polymorphic properties.

Programming Exercises

  1. Play around with the example!
  2. Test behavior if you change what is virtual, which classes contain what data, and how they behave when combined polymorphically. Try using the animal’s name and ID, and manipulating them within the dog and cat. Try creating derived classes from “cat” or “dog.”

  3. Create your own polymorphic inheritance tree to represent some other hierarchy. For example, entities in a game, or vehicles, or employees, or more animals.